-module(katzenreis_datetime).

-export([datetime_format/1, datetime_format/2,
  local_time/0, iso_datetime/0, iso_datetime/1, iso_datetime/8,
  rfc_datetime/0, rfc_datetime/1, rfc_datetime/8, time_offset/0]).

-include("katzenreis.hrl").


%% @type(datetime() = {Year::integer(), Month::integer(), Day::integer(),
%%  Hour::integer(), Min::integer(), Sec::integer(), MicroSec::integer(),
%%  DayOfWeek::integer(), Offset::integer()}).

-type(datetime() :: {Year::integer(), Month::integer(), Day::integer(),
  Hour::integer(), Min::integer(), Sec::integer(), MicroSec::integer(),
  DayOfWeek::integer(), Offset::integer()}).

-spec local_time() -> datetime().

local_time() ->
  Now = {_MegaSec, _EpochSec, MicroSec} = now(),
  {{Year, Mon, Day}, {Hour, Min, Sec}} = calendar:now_to_local_time(Now),
  Offset = time_offset(),
  DoW = calendar:day_of_the_week(Year, Mon, Day),
  {Year, Mon, Day, Hour, Min, Sec, MicroSec, DoW, Offset}.

-spec time_offset() -> integer().

time_offset() ->
  L = calendar:local_time(),
  U = calendar:universal_time(),
  {DiffDays, {DiffHours, DiffMins, DiffSecs}} =
    calendar:time_difference(U, L),
  DiffDays * 24 * 60 * 60 + DiffHours * 60 * 60 +
    DiffMins * 60 + DiffSecs.


%% @equiv rfc_datetime(local_time())

-spec rfc_datetime() -> string().

rfc_datetime() -> rfc_datetime(local_time()).

-spec rfc_datetime(datetime()) -> string().

rfc_datetime({Year, Mon, Day, Hour, Min, Sec, _MicroSec, DoW, Offset}) ->
  rfc_datetime(Year, Mon, Day, Hour, Min, Sec, DoW, Offset).

-spec rfc_datetime(integer(), integer(), integer(), integer(),
  integer(), integer(), integer(), integer()) -> string().

rfc_datetime(Year, Mon, Day, Hour, Min, Sec, DoW, Offset) ->
  Diff = datetime_rfc_offset_diff(Offset),
  F = io_lib:format("~s, ~2.10.0b ~s ~4.10.0b ~2.10.0b:~2.10.0b:~2.10.0b ~s",
    [day_of_week_name(DoW), Day, month_name(Mon), Year, Hour, Min, Sec, Diff]),
  lists:flatten(F).

datetime_offset_diff(Offset) ->
  Sign = if 0 =< Offset -> $+; true -> $- end,
  DiffHour = Offset div 60 div 60,
  DiffMin = (Offset - DiffHour * 60 * 60) div 60,
  {Sign, DiffHour, DiffMin}.

datetime_rfc_offset_diff(Offset) ->
  {Sign, DiffHour, DiffMin} = datetime_offset_diff(Offset),
  F = io_lib:format("~c~2.10.0b~2.10.0b", [Sign, DiffHour, DiffMin]),
  lists:flatten(F).

%% @equiv iso_datetime(local_time())

-spec iso_datetime() -> string().

iso_datetime() -> iso_datetime(local_time()).

-spec iso_datetime(datetime()) -> string().

iso_datetime({Year, Mon, Day, Hour, Min, Sec, MicroSec, _DoW, Offset}) ->
  iso_datetime(Year, Mon, Day, Hour, Min, Sec, MicroSec, Offset).

iso_datetime(Year, Mon, Day, Hour, Min, Sec, MicroSec, Offset) ->
  {Sign, DiffHour, DiffMin} = datetime_offset_diff(Offset),
  F = io_lib:format("~4.10.0b-~2.10.0b-~2.10.0b ~2.10.0b:~2.10.0b:~2.10.0b"
    ".~6.10.0b~c~2.10.0b:~2.10.0b",
    [Year, Mon, Day, Hour, Min, Sec, MicroSec, Sign, DiffHour, DiffMin]),
  lists:flatten(F).

day_of_week_name(0) -> "Sun";
day_of_week_name(1) -> "Mon";
day_of_week_name(2) -> "Tue";
day_of_week_name(3) -> "Web";
day_of_week_name(4) -> "Thu";
day_of_week_name(5) -> "Fri";
day_of_week_name(6) -> "Sat".

full_day_of_week_name(0) -> "Sunday";
full_day_of_week_name(1) -> "Monday";
full_day_of_week_name(2) -> "Tuesday";
full_day_of_week_name(3) -> "Wednesday";
full_day_of_week_name(4) -> "Thursday";
full_day_of_week_name(5) -> "Friday";
full_day_of_week_name(6) -> "Saturday".

month_name(1) -> "Jan";
month_name(2) -> "Feb";
month_name(3) -> "Mar";
month_name(4) -> "Apr";
month_name(5) -> "May";
month_name(6) -> "Jun";
month_name(7) -> "Jul";
month_name(8) -> "Aug";
month_name(9) -> "Sep";
month_name(10) -> "Oct";
month_name(11) -> "Nov";
month_name(12) -> "Dec".

full_month_name(1) -> "January";
full_month_name(2) -> "February";
full_month_name(3) -> "March";
full_month_name(4) -> "April";
full_month_name(5) -> "May";
full_month_name(6) -> "June";
full_month_name(7) -> "July";
full_month_name(8) -> "August";
full_month_name(9) -> "September";
full_month_name(10) -> "October";
full_month_name(11) -> "November";
full_month_name(12) -> "December".


%% @equiv datetime_format(Format, local_time())
%% @see local_time/0

-spec datetime_format(string()) -> string().

datetime_format(Format) -> datetime_format(Format, local_time()).

-spec datetime_format(string(), datetime()) -> string().

datetime_format(Format, DateTime) ->
  datetime_format1(Format, DateTime, []).

datetime_format1([], _DateTime, Expand) ->
  lists:flatten(lists:reverse(Expand));
datetime_format1([$\\, C|Format], DateTime, Expand) ->
  datetime_format1(Format, DateTime, [C|Expand]);
datetime_format1([$a|Format], DateTime, Expand) ->
  M = datetime_meridiem(DateTime),
  datetime_format1(Format, DateTime, [M|Expand]);
datetime_format1([$A|Format], DateTime, Expand) ->
  M = string:to_upper(datetime_meridiem(DateTime)),
  datetime_format1(Format, DateTime, [M|Expand]);
datetime_format1([$b|Format], DateTime, Expand) ->
  S = month_name(element(2, DateTime)),
  S1 = string:to_lower(S),
  datetime_format1(Format, DateTime, [S1|Expand]);
datetime_format1([$d|Format], DateTime, Expand) ->
  S = zero_padding(element(2, DateTime)),
  datetime_format1(Format, DateTime, [S|Expand]);
datetime_format1([$D|Format], DateTime, Expand) ->
  S = day_of_week_name(element(8, DateTime)),
  datetime_format1(Format, DateTime, [S|Expand]);
datetime_format1([$f|Format], DateTime, Expand) ->
  H = datetime_12hour(DateTime),
  S = integer_to_list(H),
  M = element(5, DateTime),
  S1 = case M =:= 0 of
    true -> S;
    false -> S ++ ":" ++ integer_to_list(M)
  end,
  datetime_format1(Format, DateTime, [S1|Expand]);
datetime_format1([$F|Format], DateTime, Expand) ->
  S = full_month_name(element(2, DateTime)),
  datetime_format1(Format, DateTime, [S|Expand]);
datetime_format1([$g|Format], DateTime, Expand) ->
  S = integer_to_list(datetime_12hour(DateTime)),
  datetime_format1(Format, DateTime, [S|Expand]);
datetime_format1([$G|Format], DateTime, Expand) ->
  S = integer_to_list(element(4, DateTime)),
  datetime_format1(Format, DateTime, [S|Expand]);
datetime_format1([$h|Format], DateTime, Expand) ->
  H = datetime_12hour(DateTime),
  S = zero_padding(H),
  datetime_format1(Format, DateTime, [S|Expand]);
datetime_format1([$H|Format], DateTime, Expand) ->
  H = element(4, DateTime),
  S = zero_padding(H),
  datetime_format1(Format, DateTime, [S|Expand]);
datetime_format1([$i|Format], DateTime, Expand) ->
  M = element(5, DateTime),
  S = zero_padding(M),
  datetime_format1(Format, DateTime, [S|Expand]);
datetime_format1([$j|Format], DateTime, Expand) ->
  M = element(2, DateTime),
  S = integer_to_list(M),
  datetime_format1(Format, DateTime, [S|Expand]);
datetime_format1([$l|Format], DateTime, Expand) ->
  S = full_day_of_week_name(element(8, DateTime)),
  datetime_format1(Format, DateTime, [S|Expand]);
datetime_format1([$L|Format], DateTime, Expand) ->
  Y = element(1, DateTime),
  S = case calendar:is_leap_year(Y) of
    true -> "true";
    false -> "false"
  end,
  datetime_format1(Format, DateTime, [S|Expand]);
datetime_format1([$m|Format], DateTime, Expand) ->
  M = element(2, DateTime),
  S = zero_padding(M),
  datetime_format1(Format, DateTime, [S|Expand]);
datetime_format1([$M|Format], DateTime, Expand) ->
  S = month_name(element(2, DateTime)),
  datetime_format1(Format, DateTime, [S|Expand]);
datetime_format1([$n|Format], DateTime, Expand) ->
  S = integer_to_list(element(2, DateTime)),
  datetime_format1(Format, DateTime, [S|Expand]);
datetime_format1([$O|Format], DateTime, Expand) ->
  S = datetime_rfc_offset_diff(element(9, DateTime)),
  datetime_format1(Format, DateTime, [S|Expand]);
datetime_format1([$r|Format], DateTime, Expand) ->
  S = rfc_datetime(DateTime),
  datetime_format1(Format, DateTime, [S|Expand]);
datetime_format1([$s|Format], DateTime, Expand) ->
  S = zero_padding(element(6, DateTime)),
  datetime_format1(Format, DateTime, [S|Expand]);
datetime_format1([$S|Format], DateTime, Expand) ->
  S = month_ordinal_suffix(element(2, DateTime)),
  datetime_format1(Format, DateTime, [S|Expand]);
datetime_format1([$t|Format], DateTime, Expand) ->
  Y = element(1, DateTime),
  M = element(2, DateTime),
  S = integer_to_list(calendar:last_day_of_the_month(Y, M)),
  datetime_format1(Format, DateTime, [S|Expand]);
datetime_format1([$w|Format], DateTime, Expand) ->
  S = integer_to_list(element(8, DateTime)),
  datetime_format1(Format, DateTime, [S|Expand]);
% TODO: implement "W"
datetime_format1([$W|Format], DateTime, Expand) ->
  S = "(W)",
  datetime_format1(Format, DateTime, [S|Expand]);
datetime_format1([$y|Format], DateTime, Expand) ->
  S = zero_padding(element(1, DateTime) rem 100),
  datetime_format1(Format, DateTime, [S|Expand]);
datetime_format1([$Y|Format], DateTime, Expand) ->
  S = integer_to_list(element(1, DateTime)),
  datetime_format1(Format, DateTime, [S|Expand]);
datetime_format1([$z|Format], DateTime, Expand) ->
  Y = element(1, DateTime),
  M = element(2, DateTime),
  D = element(3, DateTime),
  S = integer_to_list(days_of_year(Y, M, D)),
  datetime_format1(Format, DateTime, [S|Expand]);
datetime_format1([$Z|Format], DateTime, Expand) ->
  Offset = element(9, DateTime),
  S = integer_to_list(Offset),
  datetime_format1(Format, DateTime, [S|Expand]);
datetime_format1([C|Format], DateTime, Expand) ->
  datetime_format1(Format, DateTime, [C|Expand]).

datetime_meridiem(DateTime) ->
  Hour = element(4, DateTime),
  case Hour =< 12 of
    true -> "am";
    false -> "pm"
  end.

datetime_12hour(DateTime) ->
  H = element(4, DateTime),
  case H > 12 of
    true -> H - 12;
    false -> H
  end.

zero_padding(N) ->
  case N < 10 of
    true -> [$0|integer_to_list(N)];
    false -> integer_to_list(N)
  end.

month_ordinal_suffix(1) -> "st";
month_ordinal_suffix(2) -> "nd";
month_ordinal_suffix(3) -> "rd";
month_ordinal_suffix(_) -> "th".

days_of_year(Y, M, D) -> days_of_year1(Y, M-1, 0) + D.

days_of_year1(_Y, M, R) when M =< 0-> R;
days_of_year1(Y, M, R) ->
  D = calendar:last_day_of_the_month(Y, M),
  days_of_year(Y, M-1, R+D).

